package uk.org.brindy.osgibb;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import org.apache.tools.ant.Project;

import sun.misc.Service;

public class OSGiBundleBuilder {

	public static final String VERSION = "1.0.5";

	private String destdir;

	private String basedir;

	private String bundleprop;

	private String versionprop;

	private boolean svnRevision;

	private Project project;

	public OSGiBundleBuilder(Project project) {
		this.project = project;
	}

	public void setVersionprop(String versionprop) {
		this.versionprop = versionprop;
	}

	public void setBundleprop(String bundleprop) {
		this.bundleprop = bundleprop;
	}

	public void setDestdir(String destdir) {
		this.destdir = destdir;
	}

	public void setBasedir(String basedir) {
		this.basedir = basedir;
	}

	public void setSvnRevision(boolean svnRevision) {
		this.svnRevision = svnRevision;
	}

	public void execute() {
		File destDir = new File(destdir);
		File base = new File(basedir == null ? "." : basedir);
		File buildPropertiesFile = new File(base, "build.properties");
		File manifestFile = new File(base, "META-INF/MANIFEST.MF");

		// validate the files
		basicValidation(destDir, buildPropertiesFile, manifestFile);

		// read the meta files
		Manifest manifest = readManifest(manifestFile);
		Properties buildProperties = readBuildProperties(buildPropertiesFile);

		// work out the bundle version we're going to use
		String bundleVersion = handleBundleVersion(base, manifest);
		if (null != versionprop) {
			project.setProperty(versionprop, bundleVersion);
		}

		// build the bundle file name
		String bundleName = getBundleName(bundleVersion, manifest);
		File bundleFile = new File(destDir, bundleName);
		try {
			System.out.println("Building bundle: "
					+ bundleFile.getCanonicalPath());
		} catch (IOException e) {
			throw new OSGiBundleBuilderException(e);
		}

		// set the created by attribute
		manifest.getMainAttributes().put(new Attributes.Name("Created-By"),
				OSGiBundleBuilder.class.getName() + " (brindy@brindy.org.uk)");

		// create the jar file...
		JarOutputStream jarOS = openJarOutputStream(bundleFile, manifest);

		try {
			addResources(base, buildProperties, jarOS);
		} finally {
			try {
				jarOS.close();
			} catch (IOException e) {
				throw new OSGiBundleBuilderException(e);
			}
		}

	}

	private String handleBundleVersion(File base, Manifest manifest) {

		// if svn revision is required, set it
		if (svnRevision) {
			updateBuildVersionFromSVN(base, manifest);
		}

		return manifest.getMainAttributes().getValue("Bundle-Version");
	}

	@SuppressWarnings("unchecked")
	private void updateBuildVersionFromSVN(File path, Manifest manifest) {

		String bundleVersion = manifest.getMainAttributes().getValue(
				"Bundle-Version");
		if (null == bundleVersion) {
			throw new OSGiBundleBuilderException(
					"No Bundle-Version attribute found.");
		}

		String[] ver = bundleVersion.split("\\.");
		if (ver.length != 3) {
			throw new OSGiBundleBuilderException("Version is invalid format.");
		}

		Iterator iter = Service.providers(SubversionRevisionProvider.class);
		if (!iter.hasNext()) {
			throw new OSGiBundleBuilderException(
					"No SubversionRevisionProvider available");
		}

		SubversionRevisionProvider provider = (SubversionRevisionProvider) iter
				.next();

		String version = ver[0] + "." + ver[1] + "."
				+ provider.getRevision(path);
		System.out.println("Setting Bundle-Version: " + version);
		manifest.getMainAttributes().put(new Attributes.Name("Bundle-Version"),
				version);

	}

	private void addResources(File base, Properties buildProperties,
			JarOutputStream jarOS) {
		// add the classes (build.properties@output..)
		String output = buildProperties.getProperty("output..");
		if (null == output) {
			// nothing to add.
			System.out.println("No output.. resources to add");
		} else {
			File classDir = new File(base, output);
			addDirectory(jarOS, classDir, classDir, true);
		}

		// add all the binary includes (build.properties@bin.includes)
		String binIncludes = buildProperties.getProperty("bin.includes");
		if (null == binIncludes) {
			System.out.println("No bin.includes to add");
		} else {
			StringTokenizer strtok = new StringTokenizer(binIncludes, ",");
			while (strtok.hasMoreTokens()) {
				String token = strtok.nextToken();
				File include = new File(base, token);
				if (".".equals(token)) {
					continue;
				} else if (include.isDirectory()) {
					addDirectory(jarOS, base, include, false);
				} else if (include.isFile()) {
					addFile(jarOS, base, include);
				}
			}
		}
	}

	private void addDirectory(JarOutputStream jos, File base, File dir,
			boolean recurse) {
		File[] files = dir.listFiles();
		for (File file : files) {
			if (file.isFile()) {
				addFile(jos, base, file);
			}
		}

		if (recurse) {
			for (File file : files) {
				if (file.isDirectory()) {
					addDirectory(jos, base, file, recurse);
				}
			}
		}
	}

	private void addFile(JarOutputStream jos, File base, File file) {
		String entry = file.getPath().substring(base.getPath().length() + 1)
				.replace('\\', '/');

		if ("META-INF/MANIFEST.MF".equalsIgnoreCase(entry)) {
			System.out.println("Skipping already added [" + entry + "]");
			return;
		}

		System.out.println("Adding entry: [" + entry + "]");
		try {
			// create an entry
			JarEntry jarEntry = new JarEntry(entry);
			// jarEntry.setSize(file.length());
			// jarEntry.setMethod(JarEntry.STORED);
			jos.putNextEntry(jarEntry);

			// read the file and write it to the jos
			FileInputStream fis = new FileInputStream(file);
			try {
				byte[] buffer = new byte[1024];
				int read = 0;
				while (-1 != (read = fis.read(buffer))) {
					jos.write(buffer, 0, read);
				}
			} finally {
				// end the entry
				jos.closeEntry();

				// close the file
				fis.close();
			}
		} catch (IOException e) {
			throw new OSGiBundleBuilderException(e);
		}

	}

	private JarOutputStream openJarOutputStream(File bundleFile,
			Manifest manifest) {

		try {
			if (null != project && null != bundleprop) {
				project.setProperty(bundleprop, bundleFile.getCanonicalPath());
			}

			return new JarOutputStream(new FileOutputStream(bundleFile),
					manifest);
		} catch (IOException e) {
			try {
				throw new OSGiBundleBuilderException(
						"Unable to write bundle file: ["
								+ bundleFile.getCanonicalPath() + "]");
			} catch (IOException e1) {
				throw new OSGiBundleBuilderException(e1);
			}
		}
	}

	private Properties readBuildProperties(File buildPropertiesFile) {
		Properties buildProperties = new Properties();
		try {
			buildProperties.load(new FileInputStream(buildPropertiesFile));
		} catch (IOException e) {
			try {
				throw new OSGiBundleBuilderException("Unable to read ["
						+ buildPropertiesFile.getCanonicalPath() + "]", e);
			} catch (IOException e1) {
				throw new OSGiBundleBuilderException(e1);
			}
		}
		return buildProperties;
	}

	private String getBundleName(String version, Manifest mf) {
		StringBuffer bundleName = new StringBuffer();

		String[] values = mf.getMainAttributes()
				.getValue("Bundle-SymbolicName").split("\\;");

		bundleName.append(values[0].trim());
		bundleName.append("_");

		bundleName.append(version);
		bundleName.append(".jar");
		return bundleName.toString();
	}

	private Manifest readManifest(File manifest) {
		InputStream in = null;
		try {
			in = new FileInputStream(manifest);
			return new Manifest(in);
		} catch (IOException e) {
			try {
				throw new OSGiBundleBuilderException(
						"Unable to read manifest ["
								+ manifest.getCanonicalPath() + "]");
			} catch (IOException e1) {
				throw new OSGiBundleBuilderException(e1);
			}
		} finally {
			if (null != in) {
				try {
					in.close();
				} catch (IOException e) {
					// ignore
				}
			}
		}
	}

	private void basicValidation(File destDir, File buildProperties,
			File manifest) {
		if (null == destdir) {
			throw new OSGiBundleBuilderException(
					"No destination directory (destdir) has been set");
		}

		if (!destDir.isDirectory()) {
			try {
				throw new OSGiBundleBuilderException("["
						+ destDir.getCanonicalPath() + "] is not a directory");
			} catch (IOException e) {
				throw new OSGiBundleBuilderException(e);
			}
		}

		checkCanRead(buildProperties);

		checkCanRead(manifest);
	}

	private void checkCanRead(File file) {
		if (!file.canRead()) {
			try {
				throw new OSGiBundleBuilderException("Cannot read ["
						+ file.getCanonicalPath() + "]");
			} catch (IOException e) {
				throw new OSGiBundleBuilderException(e);
			}
		}
	}

	public static void main(String[] args) {
		OSGiBundleBuilder builder = new OSGiBundleBuilder(null);

		switch (args.length) {
		case 1:
			builder.setDestdir(args[0]);
			break;

		case 3:
			builder.setBasedir(args[0]);
			builder.setSvnRevision(Boolean.parseBoolean(args[1]));
			builder.setDestdir(args[2]);
			break;

		default:
			System.err.println("Error: Missing arguments");
			usage();
			return;
		}

		try {
			builder.execute();
		} catch (Exception ex) {
			ex.printStackTrace();
			usage();
		}
	}

	private static void usage() {
		System.err.println();
		System.err.println("osgibb_" + VERSION);
		System.err.println("USAGE: java -jar osgibb_" + VERSION
				+ ".jar [basedir svnRevision(true|false)] destdir");
		System.err.println();
		System.err
				.println("Note: current directory is used as the basedir if not specified");

	}

}
